/**
 * Aptana Studio
 * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
 * Please see the license.html included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package com.aptana.scripting;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.jruby.embed.LocalContextScope;
import org.jruby.embed.LocalVariableBehavior;
import org.jruby.embed.ScriptingContainer;
import org.osgi.framework.Bundle;

import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.CollectionsUtil;
import com.aptana.core.util.EclipseUtil;
import com.aptana.core.util.IConfigurationElementProcessor;
import com.aptana.core.util.ResourceUtil;
import com.aptana.scripting.model.RunType;
import com.aptana.scripting.model.ScriptLoadJob;

public class ScriptingEngine
{
	// framework_file extension point
	private static final String FRAMEWORK_FILE_ID = "frameworkFiles"; //$NON-NLS-1$
	private static final String TAG_FILE = "file"; //$NON-NLS-1$
	private static final String ATTR_NAME = "name"; //$NON-NLS-1$

	// loadpath extension point
	private static final String LOADPATH_ID = "loadPaths"; //$NON-NLS-1$
	private static final String TAG_LOADPATH = "loadPath"; //$NON-NLS-1$
	private static final String ATTR_PATH = "path"; //$NON-NLS-1$

	private static ScriptingEngine instance;

	private ScriptingContainer _scriptingContainer;
	private List<String> _loadPaths;
	private List<String> _frameworkFiles;
	private RunType _runType;
	private boolean initialized;

	/**
	 * ScriptingEngine
	 */
	private ScriptingEngine()
	{
		this._runType = ScriptingActivator.getDefaultRunType();
	}

	/**
	 * createScriptingContainer
	 * 
	 * @param scope
	 * @return
	 */
	private ScriptingContainer createScriptingContainer(LocalContextScope scope)
	{
		// ScriptingContainer result = new ScriptingContainer(scope, LocalVariableBehavior.PERSISTENT);
		ScriptingContainer result = new ScriptingContainer(scope, LocalVariableBehavior.TRANSIENT);

		try
		{
			File jrubyHome = null;
			Bundle jruby = Platform.getBundle("org.jruby"); //$NON-NLS-1$
			// try just exploding the jruby lib dir
			URL url = FileLocator.find(jruby, new Path("lib"), null); //$NON-NLS-1$

			if (url != null)
			{
				File lib = ResourceUtil.resourcePathToFile(url);
				// Ok, now use the parent of exploded lib dir as JRuby Home
				jrubyHome = lib.getParentFile();
			}
			else
			{
				// Ok, just assume the plugin is unpacked and pass the root of the plugin as JRuby Home
				jrubyHome = FileLocator.getBundleFile(jruby);
			}

			result.setHomeDirectory(jrubyHome.getAbsolutePath());

			// TODO Generate two containers? A global one for loading bundles, a threadsafe one for executing
			// commands/snippets/etc?
			// Pre-load 'ruble' framework files!
			List<String> loadPaths = result.getLoadPaths();
			loadPaths.addAll(0, getContributedLoadPaths());
			result.setLoadPaths(loadPaths);
		}
		catch (IOException e)
		{
			String message = MessageFormat.format(Messages.ScriptingEngine_Error_Setting_JRuby_Home,
					new Object[] { e.getMessage() });

			IdeLog.logError(ScriptingActivator.getDefault(), message, e);
			ScriptLogger.logError(message);
		}

		return result;
	}

	/**
	 * getContributedLoadPaths
	 * 
	 * @return
	 */
	public synchronized List<String> getContributedLoadPaths()
	{
		if (this._loadPaths == null)
		{
			final List<String> paths = new ArrayList<String>();

			// @formatter:off
			EclipseUtil.processConfigurationElements(
				ScriptingActivator.PLUGIN_ID,
				LOADPATH_ID,
				new IConfigurationElementProcessor()
				{
					public void processElement(IConfigurationElement element)
					{
						String path = element.getAttribute(ATTR_PATH);

						IExtension declaring = element.getDeclaringExtension();
						String declaringPluginID = declaring.getNamespaceIdentifier();
						Bundle bundle = Platform.getBundle(declaringPluginID);
						URL url = bundle.getEntry(path);
						String urlAsPath = ResourceUtil.resourcePathToString(url);

						if (urlAsPath != null && urlAsPath.length() > 0)
						{
							paths.add(urlAsPath);
						}
						else
						{
							String message = MessageFormat.format(
									Messages.ScriptingEngine_Unable_To_Convert_Load_Path, declaringPluginID, url);

							IdeLog.logError(ScriptingActivator.getDefault(), message);
						}
					}

					public Set<String> getSupportElementNames()
					{
						return CollectionsUtil.newSet(TAG_LOADPATH);
					}
				}
			);
			// @formatter:on

			this._loadPaths = Collections.unmodifiableList(paths);
		}

		return this._loadPaths;
	}

	/**
	 * getFrameworkFiles Used by "ruble.rb" DO NOT REMOVE!
	 * 
	 * @return
	 */
	public synchronized List<String> getFrameworkFiles()
	{
		if (this._frameworkFiles == null)
		{
			final List<String> names = new ArrayList<String>();

			// @formatter:off
			EclipseUtil.processConfigurationElements(
				ScriptingActivator.PLUGIN_ID,
				FRAMEWORK_FILE_ID,
				new IConfigurationElementProcessor()
				{
					public void processElement(IConfigurationElement element)
					{
						names.add(element.getAttribute(ATTR_NAME));
					}

					public Set<String> getSupportElementNames()
					{
						return CollectionsUtil.newSet(TAG_FILE);
					}
				}
			);
			// @formatter:on

			this._frameworkFiles = Collections.unmodifiableList(names);
		}

		return this._frameworkFiles;
	}

	/**
	 * getInstance
	 * 
	 * @return
	 */
	public static synchronized ScriptingEngine getInstance()
	{
		if (instance == null)
		{
			instance = new ScriptingEngine();
		}

		return instance;
	}

	/**
	 * getScriptingContainer
	 * 
	 * @return
	 */
	public synchronized ScriptingContainer getScriptingContainer()
	{
		if (this._scriptingContainer == null)
		{
			this._scriptingContainer = this.createScriptingContainer(LocalContextScope.SINGLETON);
		}

		return this._scriptingContainer;
	}

	/**
	 * runScript
	 * 
	 * @param fullPath
	 */
	public Object runScript(String fullPath, List<String> loadPaths)
	{
		return this.runScript(fullPath, loadPaths, this._runType, false);
	}

	/**
	 * runScript
	 * 
	 * @param fullPath
	 * @param loadPaths
	 * @param async
	 * @return
	 */
	public Object runScript(String fullPath, List<String> loadPaths, boolean async)
	{
		return this.runScript(fullPath, loadPaths, this._runType, async);
	}

	/**
	 * runScript
	 * 
	 * @param fullPath
	 * @param loadPaths
	 * @param runType
	 * @param async
	 * @return
	 */
	public Object runScript(String fullPath, List<String> loadPaths, RunType runType, boolean async)
	{
		ScriptLoadJob job = new ScriptLoadJob(fullPath, loadPaths);

		try
		{
			job.run("Load '" + fullPath + "'", runType, async); //$NON-NLS-1$ //$NON-NLS-2$
		}
		catch (InterruptedException e)
		{
			String message = MessageFormat.format(Messages.ScriptingEngine_Error_Executing_Script,
					new Object[] { fullPath });

			ScriptUtils.logErrorWithStackTrace(message, e);
		}

		return (async && this._runType != RunType.CURRENT_THREAD) ? null : job.getReturnValue();
	}

	public synchronized ScriptingContainer getInitializedScriptingContainer()
	{
		ScriptingContainer sc = getScriptingContainer();
		if (!initialized)
		{
			sc.runScriptlet("require 'ruble'"); //$NON-NLS-1$
			initialized = true;
		}
		return sc;
	}
}
